﻿using System;
using System.Diagnostics;
using OverStore.Runtime.PersistenceServiceProvider;
using OverStore.Runtime.Storage;
using System.Globalization;
using System.Reflection;

namespace OverStore.Runtime.ObjectRepository.Mapping
{
    /// <summary>
    /// <see cref="IMapper{TInstance}"/> implementation uses specified delegates for gets object data and for applies data to object.
    /// </summary>
    /// <typeparam name="TInstance">The type of the instance can be mapped.</typeparam>
    public sealed class DelegateMapper<TInstance> : IMapper<TInstance>
        where TInstance : class
    {
        private readonly Action<TypedRecord, TInstance, IRegistry> _ApplyObjectDataFunction;
        private readonly Func<TInstance, string, IRegistry , object> _GetObjectDataFunction;

        /// <summary>
        /// Initializes a new instance of the <see cref="DelegateMapper{TInstance}"/> class.
        /// </summary>
        /// <param name="getObjectDataFunction">Delegate for get object data.</param>
        /// <param name="applyObjectData">Delegate for apply object data.</param>
        /// <exception cref="ArgumentNullException"><paramref name="provider"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="getObjectDataFunction"/> is null reference.</exception>
        /// <exception cref="ArgumentNullException"><paramref name="applyObjectDataFunction"/> is null reference.</exception>
        public DelegateMapper(Func<TInstance, string, IRegistry, object> getObjectDataFunction, Action<TypedRecord, TInstance, IRegistry> applyObjectData)
        {
            if (getObjectDataFunction == null)
                throw new ArgumentNullException("getObjectDataFunction");
            _GetObjectDataFunction = getObjectDataFunction;
            if (applyObjectData == null)
                throw new ArgumentNullException("applyObjectData");
            _ApplyObjectDataFunction = applyObjectData;
        }

        /// <summary>
        /// Get data of object.
        /// Operation should not checks arguments for null etc for
        /// gain performance.
        /// </summary>
        /// <param name="instance">The instance to get data.</param>
        /// <param name="registry">The object allows to perform persistence operations over instances of different types.</param>
        /// <returns>The data of instance.</returns>
        /// <exception cref="PersistingRuntimeException">Error occured during mapping.</exception>
        public IRecord GetObjectData(TInstance instance, IRegistry registry)
        {
            return new InternalRecord(instance, this, registry);
        }

        /// <summary>
        /// Apply data to object.
        /// Operation should not checks arguments for null etc for
        /// gain performance.
        /// </summary>
        /// <param name="data">The data for apply to result.</param>
        /// <param name="result">The result object for apply data to it.</param>
        /// <param name="registry">The object allows to perform persistence operations over instances of different types.</param>
        /// <exception cref="PersistingRuntimeException">Error occured during mapping.</exception>
        public void ApplyObjectData(TypedRecord data, TInstance result, IRegistry registry)
        {
            try
            {
                _ApplyObjectDataFunction(data, result, registry);
            }
            catch (Exception ex)
            {
                throw new PersistingRuntimeException(
                            String.Format(
                                CultureInfo.InvariantCulture, 
                                "Mapping error: applying object data for instance of type {0} failed.",
                                typeof(TInstance)),
                            ex);
            }
        }

        private class InternalRecord : IRecord
        {
            private readonly TInstance _Instance;
            private readonly DelegateMapper<TInstance> _Mapper;
            private readonly IRegistry _Provider;

            [DebuggerStepThrough]
            public InternalRecord(TInstance instance, DelegateMapper<TInstance> mapper, IRegistry registry)
            {
                _Instance = instance;
                _Mapper = mapper;
                _Provider = registry;
            }

            public bool TryGetValue(string name, out object result)
            {
                try
                {
                    result = _Mapper._GetObjectDataFunction(_Instance, name, _Provider);
                    if (ReferenceEquals(result, Missing.Value /* MissingValue.Value */))
                        return false;
                    return true;
                }
                catch (Exception ex)
                {
                    throw new PersistingRuntimeException(
                        String.Format(
                            CultureInfo.InvariantCulture, 
                            "Mapping error: getting object data for instance of type {0} failed.", 
                            typeof(TInstance)), 
                        ex);
                }
            }
        }
    }
}